feat(sdk): ingredient JUMBF archives, archive metadata typing#2007
feat(sdk): ingredient JUMBF archives, archive metadata typing#2007
Conversation
gpeacock
commented
Apr 2, 2026
- Add Builder write_ingredient_archive(ingredient_id) and add_ingredient_from_archive
- Tag JUMBF archives with org.contentauth.archive.metadata (archive:type builder|ingredient)
- Add labels ARCHIVE_METADATA, ARCHIVE_TYPE_BUILDER, ARCHIVE_TYPE_INGREDIENT
- Add INGREDIENT_ARCHIVE_MIME; to_archive uses builder archive type
- Internal ArchiveKind + working_store_sign(kind); scoped one-ingredient claim via scoped_for_ingredient_archive
- Preserve parent claim thumbnail and copy assertions (exclude archive metadata & box hash); merge archive resources on import
- Reader::active_archive_type for ingredient-archive checks
- Test round-trip with claim thumbnail and resource merge
- Add Builder write_ingredient_archive(ingredient_id) and add_ingredient_from_archive - Tag JUMBF archives with org.contentauth.archive.metadata (archive:type builder|ingredient) - Add labels ARCHIVE_METADATA, ARCHIVE_TYPE_BUILDER, ARCHIVE_TYPE_INGREDIENT - Add INGREDIENT_ARCHIVE_MIME; to_archive uses builder archive type - Internal ArchiveKind + working_store_sign(kind); scoped one-ingredient claim via scoped_for_ingredient_archive - Preserve parent claim thumbnail and copy assertions (exclude archive metadata & box hash); merge archive resources on import - Reader::active_archive_type for ingredient-archive checks - Test round-trip with claim thumbnail and resource merge
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2007 +/- ##
========================================
Coverage 77.45% 77.46%
========================================
Files 176 176
Lines 44142 44256 +114
========================================
+ Hits 34192 34284 +92
- Misses 9950 9972 +22 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Merging this PR will not alter performance
Comparing Footnotes
|
| let json = format!( | ||
| r#"{{"@context":{{"archive":"https://contentauth.org/ns/archive#"}},"archive:type":"{archive_type}"}}"# | ||
| ); | ||
| let archive_metadata = Metadata::new(labels::ARCHIVE_METADATA, &json)?; |
There was a problem hiding this comment.
What is this link supposed to point to?
| .filter(|a| { | ||
| let (base, _, _) = parse_label(a.label()); | ||
| base != labels::ARCHIVE_METADATA && base != labels::BOX_HASH | ||
| }) |
There was a problem hiding this comment.
How come we filter for archive metadata and only box hash? If it's an archive metadata should we error since it shouldn't be there?
| pub(crate) fn active_archive_type(&self) -> Option<String> { | ||
| let manifest = self.active_manifest()?; | ||
| let metadata: Metadata = manifest | ||
| .find_assertion(crate::assertions::labels::ARCHIVE_METADATA) | ||
| .ok()?; | ||
| metadata | ||
| .value | ||
| .get("archive:type") | ||
| .and_then(|v: &Value| v.as_str().map(str::to_owned)) | ||
| } |
| /// | ||
| /// For other `application/c2pa` stores, use [`Self::add_ingredient_from_reader`] or | ||
| /// [`Self::add_ingredient_from_stream`]. | ||
| #[async_generic] |
There was a problem hiding this comment.
Hm, should we have async variants of this function? Is there any async behavior we can expect?
There was a problem hiding this comment.
The progress report, maybe, in future, could become async?
| /// distinguished from a full builder archive when reading with [`Self::add_ingredient_from_archive`]. | ||
| /// | ||
| /// The exported manifest is **not** a lossless slice of the parent: it uses one cloned ingredient | ||
| /// and a fresh claim instance id; other ingredients are omitted. |
There was a problem hiding this comment.
What about ingredients the ingredient contains?
There was a problem hiding this comment.
"and a fresh claim instance id"?
There was a problem hiding this comment.
Shallow is the right contract. The compound content model in specs-core #2058 treats componentOf ingredients as references to independently signed child assets. A child's provenance chain lives in the child's manifest store, which standard ingredient handling copies into the parent. Sub-ingredients are not nested on the componentOf definition itself, they resolve from the manifest store at validation time.
write_ingredient_archive needs to carry the ingredient definition and its directly associated resources. Recursive sub-ingredient walking would conflate the ingredient reference with the manifest store history.
That said, scoped_for_ingredient_archive clones the entire resource store (self.resources.clone() at line 3357), which is broader than needed. @tmathern's suggestion to scope resources to the target ingredient and claim-level refs (thumbnail) is a tighter contract and avoids carrying unrelated sibling data.
On INGREDIENT_ARCHIVE_MIME as format (line 3356): format is removed in claim v2. The org.contentauth.archive.metadata assertion already carries archive:type, so the format field is redundant. Gating the assignment on claim_version or dropping it entirely avoids a forward-compat issue.
| .collect(); | ||
| scoped.definition.ingredients = vec![ingredient.clone()]; | ||
| scoped.resources = self.resources.clone(); | ||
| scoped.definition.format = INGREDIENT_ARCHIVE_MIME.to_string(); |
There was a problem hiding this comment.
Should we have a custom format here? It's removed in claim v2.
| pub const ARCHIVE_METADATA: &str = "org.contentauth.archive.metadata"; | ||
|
|
||
| /// `archive:type` value for a full manifest [`Builder`](crate::Builder) working-store archive (JUMBF). | ||
| pub const ARCHIVE_TYPE_BUILDER: &str = "builder"; |
| /// distinguished from a full builder archive when reading with [`Self::add_ingredient_from_archive`]. | ||
| /// | ||
| /// The exported manifest is **not** a lossless slice of the parent: it uses one cloned ingredient | ||
| /// and a fresh claim instance id; other ingredients are omitted. |
There was a problem hiding this comment.
"and a fresh claim instance id"?
| /// | ||
| /// For other `application/c2pa` stores, use [`Self::add_ingredient_from_reader`] or | ||
| /// [`Self::add_ingredient_from_stream`]. | ||
| #[async_generic] |
There was a problem hiding this comment.
The progress report, maybe, in future, could become async?
| }; | ||
|
|
||
| let archive_type = kind.archive_type_str(); | ||
| let json = format!( |
There was a problem hiding this comment.
Can't serde handle that formating/json?
| pub(crate) fn active_archive_type(&self) -> Option<String> { | ||
| let manifest = self.active_manifest()?; | ||
| let metadata: Metadata = manifest | ||
| .find_assertion(crate::assertions::labels::ARCHIVE_METADATA) | ||
| .ok()?; | ||
| metadata | ||
| .value | ||
| .get("archive:type") | ||
| .and_then(|v: &Value| v.as_str().map(str::to_owned)) | ||
| } |
| .cloned() | ||
| .collect(); | ||
| scoped.definition.ingredients = vec![ingredient.clone()]; | ||
| scoped.resources = self.resources.clone(); |
There was a problem hiding this comment.
Can we just clone the resources linked to that ingredient?